#!/usr/bin/env python
# -*- coding: utf-8 -*-

#
# This program is free software; you can redistribute it and/or modify  
# it under the terms of the GNU General Public License as published by  
# the Free Software Foundation; either version 2 of the License, or     
# (at your option) any later version.                                   
#                                                                         
# A copy of the license can be found in the license.txt file supplied   
# with this software or at: http://www.gnu.org/copyleft/gpl.html       
#


import celtuce.help.logging as logging
import celtuce.help.codec as codec

import celtuce.net.ioloop as ioloop
import celtuce.net.iostream as iostream

from celtuce.event import Event,ROUTE_GET,ROUTE_DIGEST,ROUTE_DEMAND,OP_RESPONSE,TS_HEART_BEAT
from celtuce.response import Response
from celtuce import KEY_MIN,KEY_MAX
from celtuce.task import Task,TASK_CREATE_TABLET,TASK_DROP_TABLET

import threading
import time, errno, socket, fcntl

try :
    from hashlib import md5 as MD5
except :
    from md5 import md5 as MD5
from bisect import bisect_left,insort
import base64

class   Tcpio(threading.Thread) :
    def __init__(self, handle_callable, tcp_port=0) :
        threading.Thread.__init__(self) 

        self._tcp_server = None

        self._io_loop = ioloop.IOLoop()

        if tcp_port :
            self._tcp_port = tcp_port
            self._tcp_server = TcpServer(handle_callable, self._io_loop)

        self._logger = logging.getLogger('celtuce')
        ## __init__()

    def run(self) :
        self._tcp_server.listen( self._tcp_port )

        self._io_loop.start()
        ## run()

    def stop(self) :
        self._io_loop.stop()
        ## stop()

    ## class Tcpio


class   TcpServer(object) :
    def __init__(self, request_callback, io_loop) :
        self._request_callback = request_callback
        self._io_loop = io_loop

        self._socket = None
        self._logger = logging.getLogger('celtuce')
        ## __init__()

    def listen(self, port, address='') :
        assert not self._socket
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
        flags = fcntl.fcntl(self._socket.fileno(), fcntl.F_GETFD)
        flags |= fcntl.FD_CLOEXEC
        fcntl.fcntl(self._socket.fileno(), fcntl.F_SETFD, flags)
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self._socket.setblocking(0)
        self._socket.bind((address, port))
        self._socket.listen(128)
        self._io_loop.add_handler(self._socket.fileno(), self._handleEvents, self._io_loop.READ)
        ## listen()

    def _handleEvents(self, fd, events):
        while True:
            try:
                connection, address = self._socket.accept()
            except socket.error, e:
                if e[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
                    return
                raise
            try:
                stream = iostream.IOStream(connection, io_loop=self._io_loop)
                TcpConnection(stream, address, self._request_callback)
            except:
                self._logger.excpt()
        ## _handleEvents()

    ## class TcpServer

class   TcpConnection(object) :
    def __init__(self, stream, address, request_callback) :
        self._stream = stream
        self._address = address
        self._request_callback = request_callback

        self._END_TAG = "\r\n\r\n"
        self._request_finished = False
        self._stream.read_until(self._END_TAG, self.__onRequest)

        self._logger = logging.getLogger('celtuce')
        ## __init__()

    def __onRequest(self, data) :
        request = codec.decode( data )
        response = self._request_callback(request)
        self.write(response)
        ## __onRequest()

    def write(self, data) :
        if data :
            response = codec.encode( data )
            self._stream.write(''.join((response, self._END_TAG)), self.__onWriteComplete)
        else :
            self.finish()
        ## write()

    def finish(self):
        if self._stream and not self._stream.writing():
            self.__finishRequest()

    def __onWriteComplete(self):
        self.__finishRequest()
        ## __onWriteComplete()

    def __finishRequest(self) :
        self._stream.close()
        self._stream = None
        ## __finishRequest()

    ## class TcpConnection


class   MetaServer(object) :
    # index of route-table
    RTI_KEY_START      = 0
    RTI_KEY_STOP       = 1
    RTI_TABLET_SERVERS = 2

    # index of tablet-server table
    TSI_IP            = 0
    TSI_TCP_PORT      = 1
    TSI_AVAIL_SPACE   = 2
    TSI_ROUTE_DIGEST  = 3
    TSI_ROUTE_DATA    = 4
    TSI_TASK_QUEUE    = 5

    def __init__(self, tcp_port) :
        self._tcpio = Tcpio(self.handleRequest, tcp_port)

        # tablet route table
        # list of 3-tuple(key_begin, key_end, host:port of tablet server)
        self._route_tablet = {}
        self._route_tablet_text = ''
        self._route_tablet_digest = None
        self.__updateRouteTableText()

        # host table for tablet-server
        #    tablet_server --> info of tablet server
        #                      ipv4, port, available-space
        #                      route table
        #                      task queue
        self._tablet_servers = {}

        self._logger = logging.getLogger('celtuce')
        self._logger.info('MetaServer listening :%d ...', tcp_port)
        ## __init__()

    def start(self) :
        self._tcpio.start()
        ## start()

    def stop(self) :
        self._logger.info('MetaServer is shutdowning ...')
        self._tcpio.stop()
        if self._tcpio.isAlive() :
            self._tcpio.join()
        ## stop()

    def run(self) :
        # do some routines
        while True :
            time.sleep(60.0)
        ## run()

    ####-----------------------------------------------------------####

    def __updateRouteTableText(self) :
        self._route_tablet_text = codec.encodeRouteTablet( self._route_tablet )
        self._route_tablet_digest = MD5( self._route_tablet_text ).hexdigest()
        ## __updateRouteTableText()


    ####-----------------------------------------------------------####

    def handleRequest(self, request) :

        request = request
        self._logger.debug('MetaServer::handleRequest %s', request)
        self._logger.debug('MetaServer::handleRequest body %s', request.body)

        response = Event()
        response.request_id = str(id(self))
        response.op = OP_RESPONSE
        response.response_to = request.request_id

        if TS_HEART_BEAT == request.op :
            response.body = self.__handleTSHeartBeat(request.body)

        elif ROUTE_GET == request.op :
            response.body = (self._route_tablet_text, self._route_tablet_digest)
        elif ROUTE_DIGEST == request.op :
            response.body = self._route_tablet_digest
        elif ROUTE_DEMAND == request.op :
            if isinstance(request.body, dict) :
                request.body = [ request.body[i] for i in range(len(request.body)) ]
            response.body = self.__handleRouteDemand(request.body)

        else :
            raise RuntimeError,'Why not handle event "%s"' % str(request)

        return response
        ## handleRequest()

    ####-----------------------------------------------------------####

    def __handleRouteDemand(self, demand) :
        self._logger.debug('MetaServer::handleRouteDemand %s', demand)
        table_name,key = demand

        # STEP. test if table_name already exists
        if table_name not in self._route_tablet :
            # table does NOT exist, try to create it
            ok = self.__assignTaskToCreateTablet(table_name, KEY_MIN, KEY_MAX)
            return ok

        # STEP. find tablet response for the key
        tablets = self._route_tablet[ table_name ]
        index = bisect_left(tablets, (key,key,None))
        if index >= len(tablets) :
            key_start,key_stop,ts_list = tablets[ -1 ]
            if (key_start < key) and (key <= key_stop) :
                # found tablet
                return True
            if key_stop == KEY_MAX :
                self._logger.error('key %s is out of bound %s(%s .. %s]', repr(key), table_name, repr(key_start), repr(key_stop))
                return False
            else :
                # try to create a tablet
                key_start_new = key_stop
                ok = self.__assignTaskToCreateTablet(table_name, key_start_new, KEY_MAX)
                return ok

        if index > 0 :
            index -= 1
        key_start,key_stop,ts_list = tablets[ index ]
        if (key_start < key) and (key <= key_stop) :
            # found tablet
            return True

        # STEP. if not found, and there is a range gap, try to create a tablet
        self._logger.error('TODO : to find range gap of table %s', repr(table_name))
        key_start_A,key_stop_A,ts_list_A = tablets[ index ]
        if index < (len(tablets) - 1) :
            key_start_B,key_stop_B,ts_list_B = tablets[ index + 1 ]
        else :
            key_start_B,key_stop_B,ts_list_B = key_start_A,key_stop_A,ts_list_A
        
        if key_stop_A < key_start_B :
            # find gap
            key_start_new = key_stop_A
            key_stop_new  = key_start_B
            ok = self.__assignTaskToCreateTablet(table_name, key_start_new, key_stop_new)
            return ok
        elif key_stop_A == key_start_B :
            # no gap
            self._logger.error('key %s is out of bound-A %s(%s .. %s]', repr(key), table_name, repr(key_start_A), repr(key_stop_A))
            self._logger.error('key %s is out of bound-B %s(%s .. %s]', repr(key), table_name, repr(key_start_B), repr(key_stop_B))
            return False
        else :
            self._logger.error('bound-A %s(%s .. %s] > bound-B %s(%s .. %s]', table_name, repr(key_start_A), repr(key_stop_A), table_name, repr(key_start_B), repr(key_stop_B))
            return False

        return False
        ## __handleRouteDemand()

    def __assignTaskToCreateTablet(self, table_name, key_start, key_stop) :
        # STEP. find 3 tablet-servers that have most available space
        ts_entries = self.__findTopSpaceAvailableTabletServer()
        if not ts_entries :
            return False

        # STEP. put the task in queue of the 3 tablet-servers
        task_op = TASK_CREATE_TABLET
        task_body = (table_name, key_start, key_stop)
        task = Task(op=task_op, body=task_body)
        for ts_entry in ts_entries :
            task_queue = ts_entry[ self.TSI_TASK_QUEUE ]
            if task_queue is None :
                ts_entry[ self.TSI_TASK_QUEUE ] = []
                task_queue = ts_entry[ self.TSI_TASK_QUEUE ]
                
            found = False
            for _task in task_queue :
                if _task.op == task.op and _task.body == task.body :
                    found = True
                    break
            if not found :
                task_queue.append( task )

        return True
        ## __assigntaskToCreateTablet()

    def __findTopSpaceAvailableTabletServer(self, top=3) :
        '''find most space available tablet-servers top-down
        '''
        ts_entries = []
        if len(self._tablet_servers) <= top :
            for ts_name,ts_entry in self._tablet_servers.iteritems() :
                ts_entries.append(ts_entry)
        else :
            self._logger.error('TODO : to find topN space-available tablet-server.')

        return ts_entries
        ## __findTopSpaceAvailableTabletServer()

    ####-----------------------------------------------------------####

    def __handleTSHeartBeat(self, data) :
        if isinstance(data, dict) :
            data = [ data[i] for i in range(len(data)) ]
        self._logger.info('TSHB %s', str(data))
        ipv4,tcp_port,available_space,route_digest,route_data = data

        ts_name = self.__getTabletServerName(ipv4, tcp_port)
        if ts_name not in self._tablet_servers :
            self.__addTabletServer(ts_name, ipv4, tcp_port)
        ts_entry = self._tablet_servers[ ts_name ]
        ts_entry[ self.TSI_AVAIL_SPACE ] = available_space
        if route_digest != ts_entry[ self.TSI_ROUTE_DIGEST ] :
            ts_route_table = codec.decodeRouteTablet( route_data ) 
            self.__updateRouteTable(ipv4, tcp_port, ts_name, ts_route_table)

            #ts_entry[ self.TSI_ROUTE_DATA ] = route_data
            ts_entry[ self.TSI_ROUTE_DIGEST ] = route_digest

        tasks = ts_entry[ self.TSI_TASK_QUEUE ]
        result = {'task':tasks}
        if tasks :
            self._logger.info('TASK %s', ' '.join(['{%s}' % t for t in tasks]))

        # clear task queue
        ts_entry[ self.TSI_TASK_QUEUE ] = []

        return result
        ## __handleTSHeartBeat()

    def __getTabletServerName(self, ipv4, tcp_port) :
        return '_'.join((str(ipv4),str(tcp_port)))
        ## __getTabletServerName()

    def __addTabletServer(self, name, ipv4, port) :
        if name not in self._tablet_servers :
            #                               IP, PORT, SPACE, R-Digest, R-Data, TaskQueue
            self._tablet_servers[ name ] = [ipv4, port, None, None, None, None]
        ## __addTabletServer()

    def __delTabletServer(self, name) :
        if name in self._tablet_servers :
            del self._tablet_servers[ name ]
        ## __delTabletServer()

    def __updateRouteTable(self, ipv4, tcp_port, ts_name, ts_route_table) :
        '''
        @param ts_name name of tablet-server
        @param ts_route_table content of route table on tablet-server
        '''
        # STEP. archive route-data for tablet-server
        self._logger.info('TODO: to archive route-data for tablet-server %s ...', repr(ts_name))

        # STEP. remove  tablet-server name from route-table
        self.__clearTabletServerFromRouteTable( ipv4, tcp_port, ts_name )

        # STEP. update route info for tablet-server
        self.__mergeTabletServerRouteTable(ipv4, tcp_port, ts_name, ts_route_table)

        # STEP. update route table text
        self.__updateRouteTableText()

        ## __updateRouteTable()

    def __clearTabletServerFromRouteTable(self, ipv4, tcp_port, ts_name) :
        '''clear any route info about the tablet-server from route table
        
        @param ts_name name of tablet-server to be cleared
        '''
        for tbl_name,tablets in self._route_tablet.iteritems() :
            for tablet_info in tablets :
                ts_list = tablet_info[ self.RTI_TABLET_SERVERS ]
                try :
                    ts_list.remove( (ipv4, tcp_port) )
                except :
                    pass
        ## __clearTabletServerFromRouteTable()

    def __mergeTabletServerRouteTable(self, ipv4, tcp_port, ts_name, ts_route_table) :
        '''merge route-table from tablet-server into main route-table

        @param ts_name name of tablet-server
        @param ts_route_table route table of tablet-server
        '''
        self._logger.info('merge route-table from %s ...', repr(ts_name))
        self._logger.debug('route-data : %s', str(ts_route_table))

        for table_name,ts_tablets in ts_route_table.iteritems() :
            if table_name not in self._route_tablet :
                self._route_tablet[ table_name ] = []
            tablets = self._route_tablet[ table_name ]

            for ts_k_start,ts_k_stop,ts_k_state in ts_tablets :
                index = bisect_left(tablets, (ts_k_start, ts_k_stop, None))
                if index >= len(tablets) :
                    # not found 
                    tablets.append( (ts_k_start, ts_k_stop, [(ipv4, tcp_port),]) )
                    continue
                if index > 0 :
                    index -= 1
                
                # find something
                tablet = tablets[ index ]
                if tablet[self.RTI_KEY_START] == ts_k_start and tablet[self.RTI_KEY_STOP] == ts_k_stop :
                    # found and match
                    ts_list = tablet[ self.RTI_TABLET_SERVERS ]
                    if (ipv4, tcp_port) not in ts_list :
                        ts_list.append( (ipv4, tcp_port) )
                    continue 
                
                # find but not match
                if tablet[self.RTI_KEY_START] >= ts_k_start and tablet[self.RTI_KEY_START] < ts_k_stop :
                    self._logger.error('Cross Route tablet for table %s : (%s .. %s] in (%s .. %s]', ts_name, repr(tablet[self.RTI_KEY_START]), repr(tablet[self.RTI_KEY_STOP]), repr(ts_k_start), repr(ts_k_stop))
                    continue
                if tablet[self.RTI_KEY_STOP] > ts_k_start and tablet[self.RTI_KEY_STOP] <= ts_k_stop :
                    self._logger.error('Cross Route tablet for table %s : (%s .. %s] in (%s .. %s]', ts_name, repr(tablet[self.RTI_KEY_START]), repr(tablet[self.RTI_KEY_STOP]), repr(ts_k_start), repr(ts_k_stop))
                    continue

                # not found
                insort(tablets, (ts_k_start, ts_k_stop, [(ipv4, tcp_port),]) )
        ## __mergeTabletServerRouteTable()


    ####-----------------------------------------------------------####

    ## class MetaServer

#######################################################################

if __name__ == '__main__' :

    from optparse import OptionParser
    parser = OptionParser()
    parser.add_option("-p", "--port", dest="port", type="int", default=14300,
                        help='port number of meta-server')
    parser.add_option("-l", "--logconf", dest="logconfig", type="string",
                        help='path to logging configure file')

    parser.add_option("-d", "--debug", dest="debug", action="store_true", default=False,
                        help='show debug information')
    parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
                        help='show detail information')

    (options, args) = parser.parse_args()
    if not options.port :
        parser.print_help( sys.stdout )
        sys.exit(0)

    if options.logconfig :
        logging.loadConfigure( options.logconfig )

    if options.debug :
        logger = logging.getLogger()
        logger.setLevel(logging.LEVEL_DEBUG)

    server = MetaServer(options.port)
    server.start()
    
    try :
        server.run()
    finally :
        server.stop()

    sys.exit(0)

